Skip to content

feat(parser): weave is an expression, not only a statement (#88 B) - #93

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/parser-weave-add-88
Jul 29, 2026
Merged

feat(parser): weave is an expression, not only a statement (#88 B)#93
hyperpolymath merged 1 commit into
mainfrom
feat/parser-weave-add-88

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Four of the five unparsed conformance/valid/ programs — v02_weave, v08_crossing, v09_twist, v12_weave_expr — use weave as a definition body:

def simple_crossing =
  weave strands a:Q, b:Q into
    (a > b)
  yield strands b:Q, a:Q

The grammar allowed weave only as a top-level statement, so all four failed. Typed strands (a:Q) and yield strands were never the problem — those already parsed. Only the position did.

Why the expression form is the right fix, not a corpus edit

My first instinct was that the corpus was wrong, since spec/grammar.ebnf also said statement. Then I checked what a weave statement actually does:

(* eval.ml *)
| WeaveBlock _wb -> (env, None)          (* no-op *)

(* typecheck.ml *)
let _weave_ty = TTangle (input_boundary, output_boundary) in
gamma                                     (* type computed, then DISCARDED *)

As a statement, weave is inert. It binds nothing, produces nothing, and its type is thrown away. The construct could be written but never used. Binding it to a name is the only thing that makes it reachable — which is precisely what the corpus assumed all along.

The statement form is kept, so this is purely additive.

Scope — no proof implications

Weave is outside the mechanised core: 0 occurrences in proofs/Tangle.lean, 0 in the TG-3 corpus. So this touches no proof obligation, and Weave joins the other non-core constructors in tg3_emit's explicit rejection branch rather than being translated.

Threaded through ast (new Weave of weave_block), parser (primary_expr), typecheck (types as the Tangle[A,B] it denotes; body checked in the strand context exactly as the statement form does), eval (the body is the value), and pretty (re-parseable form). expr_calls also traverses into a weave body — otherwise recursion inside one would be invisible to the #91 fixpoint search.

What this does NOT fix: v11_add_block

add{ 1 + 2 + 3 } is not a missing parse rule. It is the Harvard data sub-language:

own expression grammar literals, vars, arithmetic, equality, &&/||/!, calls, if/then/else
own type system Int, Float, Rational, Hex, Binary, Bool, String, Symbolic
own environments Π, with visibility rules
own typing judgement ⊢_hd
conversion layer Embed / Unembed
calling bidirectional with Tangle

288 lines of FORMAL-SEMANTICS.md across 12 sections. That is a feature, raised separately. It remains the single entry in the corpus manifest's known-gap list.

Results

before after
conformance 14/19 18/19
corpus manifest known-unparsed 5 1

(It reported a fake 3/19 before #89 fixed the runner.)

The ratchet in check-corpus.sh demanded the manifest update — it failed the build with "now parses — drop it from CONFORMANCE_KNOWN_UNPARSED" for each file. That is exactly what it was built to do.

Tests

  • weave as a definition body (the conformance form)
  • TG-4 round-trip: pretty-print then re-parse yields the same AST — needed because I changed pretty.ml
  • guard that the statement form still parses (the change is additive)

Full suite green, 0 failures; TG-3 still 1008/0. spec/grammar.ebnf updated to match, with the rationale recorded.

🤖 Generated with Claude Code

Four of the five unparsed conformance/valid programs (v02_weave,
v08_crossing, v09_twist, v12_weave_expr) use weave as a DEFINITION BODY:

    def simple_crossing =
      weave strands a:Q, b:Q into
        (a > b)
      yield strands b:Q, a:Q

The grammar only allowed it as a top-level statement, so all four failed to
parse. Typed strands and `yield strands` were never the problem — those
already worked; only the position did.

## Why the expression form is the right fix

As a statement, weave is INERT. Evaluation is a no-op returning (env, None),
and the typechecker computes its Tangle[A,B] and then discards it:

    let _weave_ty = TTangle (input_boundary, output_boundary) in
    gamma

So the construct could be written but never bound, never used, never
observed. Binding it to a name is the only thing that makes it reachable —
which is exactly what the conformance corpus has always assumed.

The statement form is kept, so this is purely additive.

## Scope

Weave is OUTSIDE the mechanised core: 0 occurrences in proofs/Tangle.lean and
0 in the TG-3 corpus. So this touches no proof obligation, and Weave is listed
with the other non-core constructors in tg3_emit's explicit rejection branch.

Threaded through ast (new `Weave of weave_block`), parser (primary_expr),
typecheck (types as the Tangle[A,B] it denotes; body checked in the strand
context as the statement form does), eval (the body IS the value), and pretty
(prints in re-parseable form). `expr_calls` also traverses into a weave body,
or recursion inside one would be invisible to the #91 fixpoint search.

Two strand printers moved above pp_expr — they had no dependency on it and
pp_expr now needs them.

## What is NOT fixed: v11_add_block

`add{ 1 + 2 + 3 }` is not a missing parse rule. It is the Harvard DATA
SUB-LANGUAGE: its own expression grammar, its own type system (Int / Float /
Rational / Hex / Binary / Bool / String / Symbolic), its own environments (Pi)
and visibility rules, a separate typing judgement, Embed/Unembed between
Harvard and Tangle types, and bidirectional calling. 288 lines of
FORMAL-SEMANTICS.md across 12 sections. That is a feature, raised separately;
it stays in the corpus manifest as the single remaining known gap.

conformance: 14/19 -> 18/19. (It was reporting a fake 3/19 before #89 fixed
the runner.) Corpus manifest tightened 5 -> 1 — the ratchet demanded it, which
is what it is for.

Tests: weave as a definition body, the TG-4 round-trip property (pretty then
re-parse yields the same AST), and a guard that the statement form still
parses. spec/grammar.ebnf updated to match, with the rationale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hyperpolymath
hyperpolymath merged commit 1223ea6 into main Jul 29, 2026
25 checks passed
@hyperpolymath
hyperpolymath deleted the feat/parser-weave-add-88 branch July 29, 2026 01:05
Comment thread compiler/lib/eval.ml
@gitar-bot

gitar-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime.
Learn more

Code Review ✅ Approved 1 resolved / 1 findings

Expands the parser to support weave as an expression, successfully parsing four conformance tests previously blocked by grammar limits. Consider addressing the minor finding where weave evaluation discards the yield output-strand reordering.

Auto-approved and auto-merge armed: No blocking issues found.
Please see Auto-approve Docs for details on setting custom approval criteria. — merges when pipeline and required approvals pass.

✅ 1 resolved
Edge Case: Weave evaluation discards the yield output-strand reordering

📄 compiler/lib/eval.ml:387-397
The new expression-position Weave evaluation returns the body value directly (VTangle/wrapped VBraid) and never consults wb.weave_outputs. The declared output boundary/permutation (e.g. yield strands b, a swapping strands) is therefore silently dropped, so two weaves that differ only in their yield ordering evaluate to identical values. This matches the interpreter's placeholder nature, but the runtime value no longer reflects the Tangle[A,B] boundary the type checker assigns. Consider applying the output-strand permutation to the resulting word, or documenting explicitly that the value model cannot represent output reordering.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@gitar-bot

gitar-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

⚠️ Gitar auto-approved this PR but could not enable auto-merge: auto-merge is disabled for this repository — enable "Allow auto-merge" in the repository settings.

@gitar-bot gitar-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitar has auto-approved this PR and enabled auto-merge (configure)

@gitar-bot gitar-bot Bot added the gitar-approved Added by Gitar label Jul 29, 2026
hyperpolymath added a commit that referenced this pull request Jul 29, 2026
…ertion (#97)

The false assertion corrected in #95 was **not an isolated slip**.
Auditing whether the conformance corpus actually *runs* — the gate only
ever checked that it *parsed* — turned up a second mathematically false
claim of exactly the same kind, plus one genuine unimplemented feature.

## The second false assertion

`conformance/valid/v16_close_mirror_reverse.tangle`:

```tangle
assert reverse(braid[s1, s2]) == braid[s2, s1]
```

`reverse` reverses the word **and** negates every exponent — it yields
the *inverse* braid, so `reverse(s1 s2) = s2⁻¹ s1⁻¹`.

Disproved by invariant: `writhe(reverse(braid[s1,s2])) = -2` while
`writhe(braid[s2,s1]) = +2`, and writhe is invariant under the braid
relations, so they cannot be the same element.

## Two authors, same mistake → a documentation failure

`examples/trefoil.tangle` and this file made the *identical* wrong
assumption. That is not carelessness. `FORMAL-SEMANTICS.md` described
the operation as:

```
|  reverse(e)     -- reverse word
```

which reads as order-only. Now stated as an identity:

```
reverse(g₁ g₂ … gₙ)  =  gₙ⁻¹ … g₂⁻¹ g₁⁻¹        i.e.  reverse(w) = w⁻¹
```

with the contrast against `mirror` (which negates **in place**) spelled
out, and the writhe argument recorded so the next reader can check it
rather than trust it.

## The systemic fix

`scripts/check-corpus.sh` now **evaluates** `conformance/valid`, not
just parses it. Parsing was never enough — a program can parse perfectly
and assert something false. Both bad assertions survived precisely
because nothing ran them.

**Verified in both directions:** reintroducing the exact false assertion
makes the gate fail (`exit 1`, *"parses but does not evaluate"*);
restoring it returns `exit 0`.

## The genuine gap

`v09_twist.tangle` uses `(~a)` to twist a named strand inside a weave.
`spec/grammar.ebnf` says *"In weave context: (~a) twists named strand
a"*, but `infer_expr` rejects any strand name used as an expression —
specified and unimplemented. Recorded in the new
`CONFORMANCE_KNOWN_UNRUNNABLE` list rather than papered over, and raised
as **#96**.

Worth noting *why* it was invisible: until #93 a weave block could not
be bound to anything, so its body was never evaluated. The construct has
literally never run.

## Regression tests

Four pinning `reverse`, chosen so they cannot pass vacuously:

| Test | Why it can't be faked |
|---|---|
| reverse is **not** order-reversal | asserts inequality with the wrong
answer |
| `reverse(reverse(w)) = w` | only holds if it genuinely inverts |
| reverse negates writhe | the invariant that disproved both false
claims |
| `mirror` negates *in place* | pins the contrast that caused the
confusion |

Suites 112 → 116, all green; TG-3 still 1008/0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jul 29, 2026
) (#101)

`conformance/valid/v09_twist.tangle` parsed but failed to typecheck:

```tangle
def twisted =
  weave strands a:Q into
    (~a)
  yield strands a:Q
```
```
Strand name 'a' cannot be used as a standalone expression
```

## The spec licenses it explicitly

`FORMAL-SEMANTICS.md` §3.10:

```
Σ(a) = (i, T)
───────────────────────────────────  [T-Twist-Strand]
Γ; Σ ⊢ (~a) : Tangle[[T], [T]]
```

and `spec/grammar.ebnf`: *"In weave context: `(~a)` twists named strand
a."*

The `Twist` case inferred its operand as an ordinary expression, and the
`Var` rule rejects strand names outright — so the strand-name guard
fired before the twist rule could apply. `[T-Twist-Strand]` is now tried
**first**, leaving `[T-Twist-Word]`/`[T-Twist-Tangle]` to the standalone
forms.

## One of a matched pair was missing

The spec presents this rule immediately alongside `[T-Self-Cross]`:

```
Σ(a) = (i, T)                          Σ(a) = (i, T)
─────────────────────────────────      ─────────────────────────────────
Γ; Σ ⊢ (~a) : Tangle[[T], [T]]         Γ; Σ ⊢ (a > a) : Tangle[[T], [T]]
```

`[T-Self-Cross]` was already implemented; its twin was not. **A test now
pins that the two agree**, so they can't drift apart again.

## Evaluation

Mirrors `Crossing`: structural, yielding the empty word. Weave bodies
only became evaluable in #93, so this construct had **never run**.

That's also defensible mathematically — a single-strand twist is
invisible to the braid *word*: it's a framing change (Reidemeister I)
that the braid group doesn't see.

Detecting it as "a `Var` the environment doesn't bind" is safe because
typechecking runs first and admits `Twist (Var a)` only when `a` is a
strand.

## Result

conformance **16/19 → 17/19 evaluating** (18/19 parse). The corpus
ratchet demanded the manifest update — exactly what it's for.
**`v11_add_block` (#94, the Harvard sub-language) is now the only
remaining gap.**

Tests: the rule itself; agreement with `[T-Self-Cross]`; a regression
guard that standalone twist on a `Word` is unchanged; and that a
nonsense twist still fails.

All suites green; corpus and RSR gates pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gitar-approved Added by Gitar

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant